#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CMakeLists_GUI
pip install pyside6
"""
import sys, pathlib, os
from PySide6.QtCore import *
from PySide6.QtWidgets import *
from PySide6.QtGui import *
from PySide6.QtWidgets import QRadioButton, QButtonGroup
TMPL = R"""#==============================================================
#  1.  工程信息：按需修改
# cmake -B build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Debug /默认留空
# cmake -B build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
# cmake --build build
#==============================================================
set(PROJ_NAME      "{proj_name}")     # 可执行文件名称
set(PROJ_CXX_STD   {cxx_std})         # C++ 标准 C++ 11 /C++14 /C++17 /C++ 20 /C++ 23
set(IMGUI_DIR      ${{CMAKE_CURRENT_SOURCE_DIR}}/{imgui_dir})  # 头文件目录
set(SRC_LIST                                    # 全部源文件
{src_list}
)

#==============================================================
#  2.  基本设定（勿动）
#==============================================================
cmake_minimum_required(VERSION 3.15)
project(${{PROJ_NAME}} CXX)
set(CMAKE_CXX_STANDARD ${{PROJ_CXX_STD}})
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${{CMAKE_BINARY_DIR}}/bin)

#==============================================================
#  3.  第三方：空属性自动注释
#==============================================================
enable_language(C)
add_library(Other_Library STATIC IMPORTED GLOBAL)
set_target_properties(Other_Library PROPERTIES
    {debug_block}
    {release_block}
    {include_block}
)

#==============================================================
#  4.  可执行目标 & 链接（勿动）
#==============================================================
add_executable(${{PROJ_NAME}} ${{SRC_LIST}})
target_include_directories(${{PROJ_NAME}} PRIVATE ${{IMGUI_DIR}})
target_link_libraries(${{PROJ_NAME}} PRIVATE
    Other_Library opengl32 gdi32 shell32 user32 comctl32 ole32 oleaut32 uuid ws2_32 winmm)

#==============================================================
#  5.  子系统选择（勿动）
#==============================================================
option(USE_GUI_SUBSYSTEM "Use Windows subsystem instead of console" {gui_sub})
if(USE_GUI_SUBSYSTEM)
    set_target_properties(${{PROJ_NAME}} PROPERTIES WIN32_EXECUTABLE TRUE)
else()
    set_target_properties(${{PROJ_NAME}} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
endif()
"""


# -------------------- 工具：相对路径 --------------------
def rel_list(abs_list, cmake_dir):
    cmake_dir = pathlib.Path(cmake_dir).resolve()
    cmake_drive = cmake_dir.anchor.upper()
    out = []
    for p in abs_list:
        p = pathlib.Path(p).resolve()
        src_drive = p.anchor.upper()
        if src_drive == cmake_drive:
            rel = os.path.relpath(p, cmake_dir)
        else:
            rel = str(p)
        out.append(str(pathlib.Path(rel).as_posix()))
    return out


# -------------------- 主窗口 --------------------
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("CMakeLists_GUI v5")
        self.resize(1000, 720)

        # 默认配置
        self.cfg = {
            "proj_name": "ImGui",
            "cxx_std": "17",
            "imgui_dir": [],
            "third_lib_debug": [],
            "third_lib_release": [],
            "third_include": [],
            "src_list": [],
            "resource": "",
            "gui_sub": "ON",
        }

        # === PATCH：状态栏 =====================================================
        self.status_bar = self.statusBar()

        # 菜单
        file_menu = self.menuBar().addMenu("文件")
        file_menu.addAction("退出", self.close)

        # 中央布局
        central = QWidget()
        self.setCentralWidget(central)
        lay = QHBoxLayout(central)

        # 左侧滚动区
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFixedWidth(360)
        control = QWidget()
        scroll.setWidget(control)
        form = QFormLayout(control)

        # 1. 工程名
        self.le_proj = QLineEdit()
        form.addRow("软件名称:", self.le_proj)

        # 2. C++ 标准
        self.le_cxx = QLineEdit("17")
        self.le_cxx.setMaxLength(2)
        self.le_cxx.setValidator(QIntValidator(11, 23))
        form.addRow("C++标准:", self.le_cxx)

        # 3. 三排独立列表
        form.addRow(self._make_list_widget("第三方 Debug 库文件(*.lib):", "list_lib_debug", ext={".lib", ".a"}))
        form.addRow(self._make_list_widget("第三方 Release 库文件(*.lib):", "list_lib_release", ext={".lib", ".a"}))
        form.addRow(self._make_list_widget("第三方 Include 库目录:", "list_inc", is_dir=True))

        # 4. 资源文件（含浏览按钮）
        res_widget = QWidget()
        res_lay = QHBoxLayout(res_widget)
        res_lay.setContentsMargins(0, 0, 0, 0)
        self.le_res = QLineEdit()
        btn_res = QPushButton("浏览...")
        btn_res.clicked.connect(self.browse_res)
        res_lay.addWidget(self.le_res, 1)
        res_lay.addWidget(btn_res)
        form.addRow("资源文件:", res_widget)
        # 4.5 ImGui 主头文件目录
        imgui_widget = QWidget()
        imgui_lay = QHBoxLayout(imgui_widget)
        imgui_lay.setContentsMargins(0, 0, 0, 0)
        self.le_imgui_dir = QLineEdit()
        btn_imgui = QPushButton("浏览...")
        btn_imgui.clicked.connect(self.browse_imgui_dir)
        imgui_lay.addWidget(self.le_imgui_dir, 1)
        imgui_lay.addWidget(btn_imgui)
        form.addRow("Include主目录:", imgui_widget)


        # 5. 源码列表
        src_widget = QWidget()
        src_lay = QVBoxLayout(src_widget)
        src_lay.setContentsMargins(0, 0, 0, 0)
        self.list_src = QListWidget()
        self.list_src.setContextMenuPolicy(Qt.CustomContextMenu)
        self.list_src.customContextMenuRequested.connect(lambda p: self.generic_menu(p, self.list_src))
        self.list_src.setAcceptDrops(True)
        self.list_src.dragEnterEvent = self.src_drag_enter
        self.list_src.dropEvent = self.src_drop
        btn_lay = QHBoxLayout()
        btn_lay.addWidget(QPushButton("浏览…", clicked=self.browse_src))
        btn_lay.addWidget(QPushButton("清空", clicked=self.list_src.clear))
        btn_lay.addStretch()
        src_lay.addWidget(QLabel("源码列表:"))
        src_lay.addWidget(self.list_src)
        src_lay.addLayout(btn_lay)
        form.addRow(src_widget)
        
        rad_widget = QWidget()
        rad_lay = QHBoxLayout(rad_widget)
        rad_lay.setContentsMargins(0, 0, 0, 0)
        self.rad_console = QRadioButton("控制台_main")
        self.rad_gui = QRadioButton("GUI_Winmain")
        self.rad_gui.setChecked(False)  # 默认 控制台
        self.btn_grp_sub = QButtonGroup(self)  # 互斥
        self.btn_grp_sub.addButton(self.rad_console)
        self.btn_grp_sub.addButton(self.rad_gui)
        rad_lay.addWidget(self.rad_console)
        rad_lay.addWidget(self.rad_gui)
        rad_lay.addStretch()
        form.addRow("子系统:", rad_widget)

        lay.addWidget(scroll)

        # 右侧预览
        right = QVBoxLayout()
        self.preview = QPlainTextEdit()
        self.preview.setFont(QFont("Consolas", 9))
        self.preview.setReadOnly(True)
        right.addWidget(self.preview)
        btn_save = QPushButton("保存 CMakeLists.txt")
        btn_save.clicked.connect(self.save_cmake)
        right.addWidget(btn_save)
        lay.addLayout(right, 1)

        # 信号
        for w in (self.le_proj, self.le_cxx, self.le_res):
            w.textChanged.connect(self.refresh)
        self.rad_console.toggled.connect(self.refresh)
        self.rad_gui.toggled.connect(self.refresh)
        for lst in (self.list_src, self.list_lib_debug, self.list_lib_release, self.list_inc):
            lst.model().rowsInserted.connect(self.refresh)
            lst.model().rowsRemoved.connect(self.refresh)

        self.cfg_to_ui()
        self.refresh()

    def browse_imgui_dir(self):
        dir_path = QFileDialog.getExistingDirectory(self, "选择 ImGui 主目录")
        if dir_path:
            self.le_imgui_dir.setText(self.rel_path(dir_path))

    # === PATCH：统一错误提示 =================================================
    def pop_error_box(self, errors: list[str]):
        """弹窗集中提示错误，可展开详情"""
        if not errors:
            return
        msg = "配置校验失败，请修正以下问题：\n\n"
        msg += "\n".join(f"· {e}" for e in errors)
        QMessageBox.critical(self, "无法继续", msg)
        self.status_bar.showMessage("❌ 配置错误，已阻止操作")

    def validate_path(self, p: str, must_exist=True, is_dir=False) -> bool:
        """通用路径检查"""
        if not p.strip():
            return False
        pp = pathlib.Path(p)
        if pp.is_absolute():
            ok = pp.exists() if must_exist else True
        else:
            pp = pathlib.Path.cwd() / pp
            ok = pp.exists() if must_exist else True
        if ok and is_dir and not pp.is_dir():
            return False
        if ok and not is_dir and must_exist and not pp.is_file():
            return False
        return ok

    def validate_cfg(self) -> list[str]:
        """返回所有错误描述，空表表示通过"""
        errs = []
        # 1. 工程名
        pn = self.le_proj.text().strip()
        if not pn or any(c in pn for c in r'\/:*?"<>|'):
            errs.append("工程名不能为空或包含非法字符")
        # 2. C++ 标准
        std = self.le_cxx.text().strip()
        if not std or int(std) not in range(11, 24):
            errs.append("C++标准需在 11~23 之间")
        # 3. 源码列表
        if self.list_src.count() == 0:
            errs.append("源码列表不能为空")
        # 4. 资源文件
        res = self.le_res.text().strip()
        if res and not self.validate_path(res, must_exist=True, is_dir=False):
            errs.append("资源文件路径无效或不存在")
        # 5. 第三方库/头目录
        for name, lst in (
            ("Debug 库", self.list_lib_debug),
            ("Release 库", self.list_lib_release),
            ("头文件目录", self.list_inc),
        ):
            for i in range(lst.count()):
                p = lst.item(i).text()
                is_dir = "目录" in name
                if not self.validate_path(p, must_exist=True, is_dir=is_dir):
                    errs.append(f"{name} 路径无效或不存在：{p}")
        return errs

    # === PATCH：保存前校验 ==================================================
    def save_cmake(self):
        errs = self.validate_cfg()
        if errs:
            self.pop_error_box(errs)
            return

        path, _ = QFileDialog.getSaveFileName(
            self, "保存 CMakeLists.txt", "CMakeLists.txt",
            "CMake file (*.txt *.cmake);;All files (*)")
        if not path:
            return

        cmake_dir = pathlib.Path(path).parent.resolve()
        self.ui_to_cfg()

        src_lines = rel_list(self.cfg["src_list"], cmake_dir)
        if self.cfg["resource"]:
            res = self.cfg["resource"]
            src_lines.insert(0,
                             str(pathlib.Path(os.path.relpath(res, cmake_dir)).as_posix())
                             if pathlib.Path(res).is_absolute() else res)

        debug_block = self._cmake_block("DEBUG_LIBS", rel_list(self.cfg["third_lib_debug"], cmake_dir))
        release_block = self._cmake_block("RELEASE_LIBS", rel_list(self.cfg["third_lib_release"], cmake_dir))
        include_block = self._cmake_block("INCLUDE_DIRS", rel_list(self.cfg["third_include"], cmake_dir))

        txt = TMPL.format(
            proj_name=self.cfg["proj_name"],
            cxx_std=self.cfg["cxx_std"],
            imgui_dir="\n    ".join(rel_list(self.cfg["imgui_dir"], cmake_dir)),
            gui_sub=self.cfg["gui_sub"],
            src_list="\n    ".join(src_lines),
            debug_block=debug_block,
            release_block=release_block,
            include_block=include_block,
        )

        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(txt)
            QMessageBox.information(self, "完成", f"已保存到\n{path}")
            self.status_bar.showMessage(f"✅ 已保存 {path}")
        except Exception as e:
            QMessageBox.critical(self, "写入失败", str(e))
            self.status_bar.showMessage("❌ 保存失败")

    # === PATCH：刷新也做轻量校验 ============================================
    def refresh(self):
        errs = self.validate_cfg()
        if errs:
            self.status_bar.showMessage("⚠️ 配置有误，保存已被阻止")
        else:
            self.status_bar.clearMessage()
        # 以下与原 refresh 逻辑相同
        self.ui_to_cfg()

        src_lines = self.cfg["src_list"].copy()
        if self.cfg["resource"]:
            src_lines.insert(0, self.cfg["resource"])
        src_list_str = "\n    ".join(src_lines) if src_lines else ""

        debug_block = self._cmake_block("DEBUG_LIBS", self.cfg["third_lib_debug"])
        release_block = self._cmake_block("RELEASE_LIBS", self.cfg["third_lib_release"])
        include_block = self._cmake_block("INCLUDE_DIRS", self.cfg["third_include"])

        txt = TMPL.format(
            proj_name=self.cfg["proj_name"],
            cxx_std=self.cfg["cxx_std"],
            imgui_dir="\n    ".join(self.cfg["imgui_dir"]),
            gui_sub=self.cfg["gui_sub"],
            src_list=src_list_str,
            debug_block=debug_block,
            release_block=release_block,
            include_block=include_block,
        )
        self.preview.setPlainText(txt)

    # -------------------- 以下与原代码相同（省略以节省篇幅） -----------------
    def browse_res(self):
        f, _ = QFileDialog.getOpenFileName(
            self, "选择资源文件", "",
            "资源文件 (*.rc *.res);;所有文件 (*)")
        if f:
            self.le_res.setText(self.rel_path(f))

    def _make_list_widget(self, title, attr, is_dir=False, ext=None):
        widget = QWidget()
        v = QVBoxLayout(widget)
        v.setContentsMargins(0, 0, 0, 0)
        lst = QListWidget()
        setattr(self, attr, lst)
        lst.setContextMenuPolicy(Qt.CustomContextMenu)
        lst.customContextMenuRequested.connect(lambda p: self.generic_menu(p, lst))
        lst.setAcceptDrops(True)
        lst.dragEnterEvent = lambda e: self.drag_accept(e, is_dir)
        lst.dropEvent = lambda e: self.drop_add(e, lst, is_dir)
        btn_lay = QHBoxLayout()
        btn_lay.addWidget(QPushButton("浏览…", clicked=lambda: self.browse_multi(lst, is_dir, ext)))
        btn_lay.addWidget(QPushButton("清空", clicked=lst.clear))
        btn_lay.addStretch()
        v.addWidget(QLabel(title))
        v.addWidget(lst)
        v.addLayout(btn_lay)
        return widget

    def generic_menu(self, pos, lst: QListWidget):
        menu = QMenu()
        item = lst.currentItem()
        if item:
            menu.addAction("删除该项", lambda: lst.takeItem(lst.row(item)))
        menu.addAction("清空全部", lst.clear)
        menu.exec(lst.mapToGlobal(pos))

    def browse_src(self):
        files, _ = QFileDialog.getOpenFileNames(
            self, "添加源文件", "",
            "Source Files (*.cpp *.c *.cc *.cxx *.h *.hpp *.rc);;All files (*)")
        for f in files:
            self.add_unique(self.list_src, self.rel_path(f))

    def browse_multi(self, lst: QListWidget, is_dir=False, ext=None):
        if is_dir:
            dir_path = QFileDialog.getExistingDirectory(self, "选择目录")
            if dir_path:
                self.add_unique(lst, self.rel_path(dir_path))
        else:
            files, _ = QFileDialog.getOpenFileNames(
                self, "选择文件", "", "All files (*)")
            for f in files:
                if ext and not pathlib.Path(f).suffix.lower() in ext:
                    continue
                self.add_unique(lst, self.rel_path(f))

    def drag_accept(self, e: QDragEnterEvent, is_dir=False):
        if e.mimeData().hasUrls():
            e.acceptProposedAction()

    def drop_add(self, e: QDropEvent, lst: QListWidget, is_dir=False):
        for u in e.mimeData().urls():
            p = pathlib.Path(u.toLocalFile())
            if is_dir and p.is_dir():
                self.add_unique(lst, self.rel_path(str(p)))
            elif not is_dir and p.is_file():
                self.add_unique(lst, self.rel_path(str(p)))

    def src_drag_enter(self, e: QDragEnterEvent):
        if e.mimeData().hasUrls():
            e.acceptProposedAction()

    def src_drop(self, e: QDropEvent):
        for u in e.mimeData().urls():
            p = pathlib.Path(u.toLocalFile())
            if p.suffix.lower() in {".cpp", ".c", ".cc", ".cxx", ".h", ".hpp", ".rc"}:
                self.add_unique(self.list_src, self.rel_path(str(p)))

    def rel_path(self, abspath: str) -> str:
        try:
            return str(pathlib.Path(abspath).relative_to(pathlib.Path.cwd())).replace("\\", "/")
        except ValueError:
            return str(abspath).replace("\\", "/")

    def add_unique(self, lst: QListWidget, path: str):
        if not lst.findItems(path, Qt.MatchExactly):
            lst.addItem(path)

    def ui_to_cfg(self):
        self.cfg["imgui_dir"] = [self.le_imgui_dir.text().strip()] if self.le_imgui_dir.text().strip() else []
        self.cfg["proj_name"] = self.le_proj.text().strip()
        self.cfg["cxx_std"] = self.le_cxx.text().strip()
        self.cfg["third_lib_debug"] = [self.list_lib_debug.item(i).text() for i in range(self.list_lib_debug.count())]
        self.cfg["third_lib_release"] = [self.list_lib_release.item(i).text() for i in range(self.list_lib_release.count())]
        self.cfg["third_include"] = [self.list_inc.item(i).text() for i in range(self.list_inc.count())]
        self.cfg["resource"] = self.le_res.text().strip()
        self.cfg["gui_sub"] = "ON" if self.rad_gui.isChecked() else "OFF"
        self.cfg["src_list"] = [self.list_src.item(i).text() for i in range(self.list_src.count())]

    def cfg_to_ui(self):
        imgui_dirs = self.cfg.get("imgui_dir", [])
        self.le_imgui_dir.setText(imgui_dirs[0] if imgui_dirs else "")
        self.le_proj.setText(self.cfg.get("proj_name", "ImGui"))
        self.le_cxx.setText(self.cfg.get("cxx_std", "17"))
        self.le_res.setText(self.cfg.get("resource", ""))
        self.rad_gui.setChecked(self.cfg.get("gui_sub", "ON") == "ON")
        self.rad_console.setChecked(self.cfg.get("gui_sub", "ON") == "OFF")
        for lst, key in ((self.list_lib_debug, "third_lib_debug"),
                         (self.list_lib_release, "third_lib_release"),
                         (self.list_inc, "third_include"),
                         (self.list_src, "src_list")):
            lst.clear()
            lst.addItems(self.cfg.get(key, []))

    def _cmake_block(self, name, values):
        if not values:
            return f"# Other_Library {name} 未指定"
        joined = ";".join(values)
        if name == "DEBUG_LIBS":
            cmake_list = ";".join(f"${{CMAKE_CURRENT_SOURCE_DIR}}/{v}" for v in values)
            return f'IMPORTED_LOCATION_DEBUG "{cmake_list}"'
        elif name == "RELEASE_LIBS":
            cmake_list = ";".join(f"${{CMAKE_CURRENT_SOURCE_DIR}}/{v}" for v in values)
            return f'IMPORTED_LOCATION_RELEASE "{cmake_list}"'
        elif name == "INCLUDE_DIRS":
            cmake_list = ";".join(f"${{CMAKE_CURRENT_SOURCE_DIR}}/{v}" for v in values)
            return f'INTERFACE_INCLUDE_DIRECTORIES "{cmake_list}"'
        return ""


# -------------------- 启动 --------------------
if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec())
